diff --git a/config/config.go b/config/config.go index 2065ddf..4c6ff62 100644 --- a/config/config.go +++ b/config/config.go @@ -1,295 +1,344 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ // Package config holds and assists in the configuration of a writefreely instance. package config import ( "net/url" "strings" "github.com/go-ini/ini" "github.com/writeas/web-core/log" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" "golang.org/x/net/idna" ) const ( // FileName is the default configuration file name FileName = "config.ini" UserNormal UserType = "user" UserAdmin = "admin" ) type ( UserType string // ServerCfg holds values that affect how the HTTP server runs ServerCfg struct { HiddenHost string `ini:"hidden_host"` Port int `ini:"port"` Bind string `ini:"bind"` TLSCertPath string `ini:"tls_cert_path"` TLSKeyPath string `ini:"tls_key_path"` Autocert bool `ini:"autocert"` TemplatesParentDir string `ini:"templates_parent_dir"` StaticParentDir string `ini:"static_parent_dir"` PagesParentDir string `ini:"pages_parent_dir"` KeysParentDir string `ini:"keys_parent_dir"` HashSeed string `ini:"hash_seed"` GopherPort int `ini:"gopher_port"` Dev bool `ini:"-"` } // DatabaseCfg holds values that determine how the application connects to a datastore DatabaseCfg struct { Type string `ini:"type"` FileName string `ini:"filename"` User string `ini:"username"` Password string `ini:"password"` Database string `ini:"database"` Host string `ini:"host"` Port int `ini:"port"` TLS bool `ini:"tls"` } WriteAsOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` AuthLocation string `ini:"auth_location"` TokenLocation string `ini:"token_location"` InspectLocation string `ini:"inspect_location"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` } GitlabOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` Host string `ini:"host"` DisplayName string `ini:"display_name"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` } GiteaOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` Host string `ini:"host"` DisplayName string `ini:"display_name"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` } SlackOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` TeamID string `ini:"team_id"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` } GenericOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` Host string `ini:"host"` DisplayName string `ini:"display_name"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` TokenEndpoint string `ini:"token_endpoint"` InspectEndpoint string `ini:"inspect_endpoint"` AuthEndpoint string `ini:"auth_endpoint"` Scope string `ini:"scope"` AllowDisconnect bool `ini:"allow_disconnect"` MapUserID string `ini:"map_user_id"` MapUsername string `ini:"map_username"` MapDisplayName string `ini:"map_display_name"` MapEmail string `ini:"map_email"` } // AppCfg holds values that affect how the application functions AppCfg struct { SiteName string `ini:"site_name"` SiteDesc string `ini:"site_description"` Host string `ini:"host"` // Site appearance Theme string `ini:"theme"` Editor string `ini:"editor"` JSDisabled bool `ini:"disable_js"` WebFonts bool `ini:"webfonts"` Landing string `ini:"landing"` SimpleNav bool `ini:"simple_nav"` WFModesty bool `ini:"wf_modesty"` // Site functionality Chorus bool `ini:"chorus"` Forest bool `ini:"forest"` // The admin cares about the forest, not the trees. Hide unnecessary technical info. DisableDrafts bool `ini:"disable_drafts"` // Users SingleUser bool `ini:"single_user"` OpenRegistration bool `ini:"open_registration"` OpenDeletion bool `ini:"open_deletion"` MinUsernameLen int `ini:"min_username_len"` MaxBlogs int `ini:"max_blogs"` // Options for public instances // Federation Federation bool `ini:"federation"` PublicStats bool `ini:"public_stats"` Monetization bool `ini:"monetization"` NotesOnly bool `ini:"notes_only"` // Access Private bool `ini:"private"` // Additional functions LocalTimeline bool `ini:"local_timeline"` UserInvites string `ini:"user_invites"` // Defaults DefaultVisibility string `ini:"default_visibility"` // Check for Updates UpdateChecks bool `ini:"update_checks"` // Disable password authentication if use only Oauth DisablePasswordAuth bool `ini:"disable_password_auth"` + + // Which Markdown renderer to use + Renderer string `ini:"markdown_renderer"` + + // Options for the Goldmark renderer + RendererOptions string `ini:"markdown_options"` + // Conversion of options ready for the renderer + rendererExtensions []goldmark.Extender } // Config holds the complete configuration for running a writefreely instance Config struct { Server ServerCfg `ini:"server"` Database DatabaseCfg `ini:"database"` App AppCfg `ini:"app"` SlackOauth SlackOauthCfg `ini:"oauth.slack"` WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"` GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"` GiteaOauth GiteaOauthCfg `ini:"oauth.gitea"` GenericOauth GenericOauthCfg `ini:"oauth.generic"` } ) // New creates a new Config with sane defaults func New() *Config { c := &Config{ Server: ServerCfg{ Port: 8080, Bind: "localhost", /* IPV6 support when not using localhost? */ }, App: AppCfg{ Host: "http://localhost:8080", Theme: "write", WebFonts: true, SingleUser: true, MinUsernameLen: 3, MaxBlogs: 1, Federation: true, PublicStats: true, }, } c.UseMySQL(true) return c } // UseMySQL resets the Config's Database to use default values for a MySQL setup. func (cfg *Config) UseMySQL(fresh bool) { cfg.Database.Type = "mysql" if fresh { cfg.Database.Host = "localhost" cfg.Database.Port = 3306 } } // UseSQLite resets the Config's Database to use default values for a SQLite setup. func (cfg *Config) UseSQLite(fresh bool) { cfg.Database.Type = "sqlite3" if fresh { cfg.Database.FileName = "writefreely.db" } } // IsSecureStandalone returns whether or not the application is running as a // standalone server with TLS enabled. func (cfg *Config) IsSecureStandalone() bool { return cfg.Server.Port == 443 && cfg.Server.TLSCertPath != "" && cfg.Server.TLSKeyPath != "" } func (ac *AppCfg) LandingPath() string { if !strings.HasPrefix(ac.Landing, "/") { return "/" + ac.Landing } return ac.Landing } func (ac AppCfg) SignupPath() string { if !ac.OpenRegistration { return "" } if ac.Chorus || ac.Private || (ac.Landing != "" && ac.Landing != "/") { return "/signup" } return "/" } +func (ac AppCfg) MarkdownRenderer() string { + if strings.EqualFold(ac.Renderer, "goldmark") { + return "goldmark" + } + return "saturday" +} + +func (ac AppCfg) RendererExtensions() []goldmark.Extender { + if ac.rendererExtensions != nil { + return ac.rendererExtensions + } + var extlist []goldmark.Extender + optlist := strings.FieldsFunc(ac.RendererOptions, func(r rune) bool { + return r == ' ' || r == '\t' || r == ',' + }) + for _, opt := range optlist { + switch opt { + case "table": + extlist = append(extlist, extension.Table) + case "strikethrough": + extlist = append(extlist, extension.Strikethrough) + case "linkify": + extlist = append(extlist, extension.Linkify) + case "tasklist": + extlist = append(extlist, extension.TaskList) + case "gfm": + extlist = append(extlist, extension.GFM) + case "definitionlist": + extlist = append(extlist, extension.DefinitionList) + case "typographer": + extlist = append(extlist, extension.Typographer) + case "cjk": + extlist = append(extlist, extension.CJK) + } + } + ac.rendererExtensions = extlist + return extlist +} + // Load reads the given configuration file, then parses and returns it as a Config. func Load(fname string) (*Config, error) { if fname == "" { fname = FileName } cfg, err := ini.Load(fname) if err != nil { return nil, err } // Parse INI file uc := &Config{} err = cfg.MapTo(uc) if err != nil { return nil, err } // Do any transformations u, err := url.Parse(uc.App.Host) if err != nil { return nil, err } d, err := idna.ToASCII(u.Hostname()) if err != nil { log.Error("idna.ToASCII for %s: %s", u.Hostname(), err) return nil, err } uc.App.Host = u.Scheme + "://" + d if u.Port() != "" { uc.App.Host += ":" + u.Port() } return uc, nil } // Save writes the given Config to the given file. func Save(uc *Config, fname string) error { cfg := ini.Empty() err := ini.ReflectFrom(cfg, uc) if err != nil { return err } if fname == "" { fname = FileName } return cfg.SaveTo(fname) } diff --git a/go.mod b/go.mod index ad9fb6a..aa0d978 100644 --- a/go.mod +++ b/go.mod @@ -1,95 +1,100 @@ module github.com/writefreely/writefreely require ( + git.mills.io/prologic/go-gopher v0.0.0-20210712135410-b7ebb55feece + github.com/clbanning/mxj v1.8.4 // indirect github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.15.0 github.com/go-ini/ini v1.67.0 github.com/go-sql-driver/mysql v1.7.1 github.com/gorilla/csrf v1.7.1 github.com/gorilla/feeds v1.1.1 github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.2.0 github.com/gorilla/sessions v1.2.1 github.com/guregu/null v4.0.0+incompatible github.com/hashicorp/go-multierror v1.1.1 github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec github.com/manifoldco/promptui v0.9.0 github.com/microcosm-cc/bluemonday v1.0.23 github.com/mitchellh/go-wordwrap v1.0.1 github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d github.com/stretchr/testify v1.8.4 github.com/urfave/cli/v2 v2.25.7 github.com/writeas/activity v0.1.2 github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481 github.com/writeas/go-strip-markdown/v2 v2.1.1 github.com/writeas/go-webfinger v1.1.0 github.com/writeas/httpsig v1.0.0 github.com/writeas/impart v1.1.1 github.com/writeas/import v0.2.1 github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219 github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320 github.com/writeas/slug v1.2.0 github.com/writeas/web-core v1.5.0 github.com/writefreely/go-gopher v0.0.0-20220429181814-40127126f83b github.com/writefreely/go-nodeinfo v1.2.0 golang.org/x/crypto v0.13.0 golang.org/x/net v0.15.0 modernc.org/sqlite v1.20.0 ) require ( code.as/core/socks v1.0.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beevik/etree v1.1.0 // indirect github.com/captncraig/cors v0.0.0-20190703115713-e80254a89df1 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect - github.com/clbanning/mxj v1.8.4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 // indirect github.com/fatih/structs v1.1.0 // indirect + github.com/forPelevin/gomoji v1.1.3 // indirect github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe // indirect github.com/go-test/deep v1.0.1 // indirect github.com/gofrs/uuid v3.3.0+incompatible // indirect github.com/gologme/log v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect github.com/writeas/go-writeas/v2 v2.0.2 // indirect github.com/writeas/openssl-go v1.0.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + github.com/yuin/goldmark v1.5.6 // indirect + go.abhg.dev/goldmark/hashtag v0.3.1 // indirect golang.org/x/mod v0.8.0 // indirect golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.6.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/uint128 v1.2.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect modernc.org/libc v1.21.5 // indirect modernc.org/mathutil v1.5.0 // indirect modernc.org/memory v1.4.0 // indirect modernc.org/opt v0.1.3 // indirect modernc.org/strutil v1.1.3 // indirect modernc.org/token v1.0.1 // indirect ) go 1.19 diff --git a/go.sum b/go.sum index 645f030..0167915 100644 --- a/go.sum +++ b/go.sum @@ -1,244 +1,256 @@ code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs= code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY= +git.mills.io/prologic/go-gopher v0.0.0-20210712135410-b7ebb55feece h1:0esmnntqeuM1iBgHH0HOeSynsLA1l28p2K3h/WZuIfQ= +git.mills.io/prologic/go-gopher v0.0.0-20210712135410-b7ebb55feece/go.mod h1:EMXlYOIbYJQhPTtIltgaaHtCYDawV/HL0dYf8ShzAck= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/captncraig/cors v0.0.0-20190703115713-e80254a89df1 h1:AFSJaASPGYNbkUa5c8ZybrcW9pP3Cy7+z5dnpcc/qG8= github.com/captncraig/cors v0.0.0-20190703115713-e80254a89df1/go.mod h1:EIlIeMufZ8nqdUhnesledB15xLRl4wIJUppwDLPrdrQ= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/clbanning/mxj v1.8.3/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs= github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/forPelevin/gomoji v1.1.3 h1:7c3dYzVmYhpOL3bS4riXqSWJBX3BhSvH68yoNNf3FH0= +github.com/forPelevin/gomoji v1.1.3/go.mod h1:ypB7Kz3Fsp+LVR7KoT7mEFOioYBuTuAtaAT4RGl+ASY= github.com/go-fed/httpsig v0.1.0/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE= github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe h1:U71giCx5NjRn4Lb71UuprPHqhjxGv3Jqonb9fgcaJH8= github.com/go-fed/httpsig v0.1.1-0.20200204213531-0ef28562fabe/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c= github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE= github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY= github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/guregu/null v4.0.0+incompatible h1:4zw0ckM7ECd6FNNddc3Fu4aty9nTlpkkzH7dPn4/4Gw= github.com/guregu/null v4.0.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 h1:wIdDEle9HEy7vBPjC6oKz6ejs3Ut+jmsYvuOoAW2pSM= github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2/go.mod h1:WtaVKD9TeruTED9ydiaOJU08qGoEPP/LyzTKiD3jEsw= github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec h1:ZXWuspqypleMuJy4bzYEqlMhJnGAYpLrWe5p7W3CdvI= github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec/go.mod h1:voECJzdraJmolzPBgL9Z7ANwXf4oMXaTCsIkdiPpR/g= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY= github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/writeas/activity v0.1.2 h1:Y12B5lIrabfqKE7e7HFCWiXrlfXljr9tlkFm2mp7DgY= github.com/writeas/activity v0.1.2/go.mod h1:mYYgiewmEM+8tlifirK/vl6tmB2EbjYaxwb+ndUw5T0= github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481 h1:BiSivIxLQFcKoUorpNN3rNwwFG5bITPnqUSyIccfdh0= github.com/writeas/activityserve v0.0.0-20200409150223-d7ab3eaa4481/go.mod h1:4akDJSl+sSp+QhrQKMqzAqdV1gJ1pPx6XPI77zgMM8o= github.com/writeas/go-strip-markdown/v2 v2.1.1 h1:hAxUM21Uhznf/FnbVGiJciqzska6iLei22Ijc3q2e28= github.com/writeas/go-strip-markdown/v2 v2.1.1/go.mod h1:UvvgPJgn1vvN8nWuE5e7v/+qmDu3BSVnKAB6Gl7hFzA= github.com/writeas/go-webfinger v1.1.0 h1:MzNyt0ry/GMsRmJGftn2o9mPwqK1Q5MLdh4VuJCfb1Q= github.com/writeas/go-webfinger v1.1.0/go.mod h1:w2VxyRO/J5vfNjJHYVubsjUGHd3RLDoVciz0DE3ApOc= github.com/writeas/go-writeas v1.1.0/go.mod h1:oh9U1rWaiE0p3kzdKwwvOpNXgp0P0IELI7OLOwV4fkA= github.com/writeas/go-writeas/v2 v2.0.2 h1:akvdMg89U5oBJiCkBwOXljVLTqP354uN6qnG2oOMrbk= github.com/writeas/go-writeas/v2 v2.0.2/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M= github.com/writeas/httpsig v1.0.0 h1:peIAoIA3DmlP8IG8tMNZqI4YD1uEnWBmkcC9OFPjt3A= github.com/writeas/httpsig v1.0.0/go.mod h1:7ClMGSrSVXJbmiLa17bZ1LrG1oibGZmUMlh3402flPY= github.com/writeas/impart v1.1.0/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y= github.com/writeas/impart v1.1.1 h1:RyA9+CqbdbDuz53k+nXCWUY+NlEkdyw6+nWanxSBl5o= github.com/writeas/impart v1.1.1/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y= github.com/writeas/import v0.2.1 h1:3k+bDNCyqaWdZinyUZtEO4je3mR6fr/nE4ozTh9/9Wg= github.com/writeas/import v0.2.1/go.mod h1:gFe0Pl7ZWYiXbI0TJxeMMyylPGZmhVvCfQxhMEc8CxM= github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219 h1:baEp0631C8sT2r/hqwypIw2snCFZa6h7U6TojoLHu/c= github.com/writeas/monday v0.0.0-20181024183321-54a7dd579219/go.mod h1:NyM35ayknT7lzO6O/1JpfgGyv+0W9Z9q7aE0J8bXxfQ= github.com/writeas/openssl-go v1.0.0 h1:YXM1tDXeYOlTyJjoMlYLQH1xOloUimSR1WMF8kjFc5o= github.com/writeas/openssl-go v1.0.0/go.mod h1:WsKeK5jYl0B5y8ggOmtVjbmb+3rEGqSD25TppjJnETA= github.com/writeas/saturday v1.7.1/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ= github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320 h1:PozPZ29CQ/xt6ym/+FvIz+KvKEObSSc5ye+95zbTjVU= github.com/writeas/saturday v1.7.2-0.20200427193424-392b95a03320/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ= github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g= github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ= github.com/writeas/web-core v1.5.0 h1:jKJObEgzYoNWUsIJmIDLAjN+vADkSsfcj7Ir9YyFcPg= github.com/writeas/web-core v1.5.0/go.mod h1:jb7dEvnyvPbKA3pTCzD1Dch5ou/n5YkkydZYhn6/NVY= github.com/writefreely/go-gopher v0.0.0-20220429181814-40127126f83b h1:h3NzB8OZ50NNi5k9yrFeyFszt3LyqyVK4+xUHFYY8B0= github.com/writefreely/go-gopher v0.0.0-20220429181814-40127126f83b/go.mod h1:T2UVVzt+R5KSSZe2xRSytnwc2M9AoDegi7foeIsik+M= github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss= github.com/writefreely/go-nodeinfo v1.2.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.5.6 h1:COmQAWTCcGetChm3Ig7G/t8AFAN00t+o8Mt4cf7JpwA= +github.com/yuin/goldmark v1.5.6/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.abhg.dev/goldmark/hashtag v0.3.1 h1:k0FQwEtVQ1SstIRR2fqDJ4VNYUS0AXLp869V0qHOZMg= +go.abhg.dev/goldmark/hashtag v0.3.1/go.mod h1:rXtvxXPL7auhPMGRdG02UrXn/9LMm6PNdP5HO64zbVU= golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/libc v1.21.5 h1:xBkU9fnHV+hvZuPSRszN0AXDG4M7nwPLwTWwkYcvLCI= modernc.org/libc v1.21.5/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk= modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sqlite v1.20.0 h1:80zmD3BGkm8BZ5fUi/4lwJQHiO3GXgIUvZRXpoIfROY= modernc.org/sqlite v1.20.0/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw= modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34= modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE= diff --git a/postrender.go b/postrender.go index 8e4d706..1e240ae 100644 --- a/postrender.go +++ b/postrender.go @@ -1,363 +1,465 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "bytes" "encoding/json" "fmt" "html" "html/template" "net/http" "net/url" "regexp" "strings" "unicode" "unicode/utf8" + hashtag "go.abhg.dev/goldmark/hashtag" "github.com/microcosm-cc/bluemonday" stripmd "github.com/writeas/go-strip-markdown/v2" "github.com/writeas/impart" blackfriday "github.com/writeas/saturday" "github.com/writeas/web-core/log" "github.com/writeas/web-core/stringmanip" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/parser" + "github.com/writefreely/writefreely/config" "github.com/writefreely/writefreely/parse" ) var ( blockReg = regexp.MustCompile("<(ul|ol|blockquote)>\n") endBlockReg = regexp.MustCompile("\n") youtubeReg = regexp.MustCompile("(https?://www.youtube.com/embed/[a-zA-Z0-9\\-_]+)(\\?[^\t\n\f\r \"']+)?") titleElementReg = regexp.MustCompile("") hashtagReg = regexp.MustCompile(`{{\[\[\|\|([^|]+)\|\|\]\]}}`) markeddownReg = regexp.MustCompile("

(.+)

") mentionReg = regexp.MustCompile(`@([A-Za-z0-9._%+-]+)(@[A-Za-z0-9.-]+\.[A-Za-z]+)\b`) ) func (p *Post) handlePremiumContent(c *Collection, isOwner, postPage bool, cfg *config.Config) { if c.Monetization != "" { // User has Web Monetization enabled, so split content if it exists spl := strings.Index(p.Content, shortCodePaid) p.IsPaid = spl > -1 if postPage { // We're viewing the individual post if isOwner { p.Content = strings.Replace(p.Content, shortCodePaid, "\n\n"+`

Your subscriber content begins here.

`+"\n\n", 1) } else { if spl > -1 { p.Content = p.Content[:spl+len(shortCodePaid)] p.Content = strings.Replace(p.Content, shortCodePaid, "\n\n"+`

Continue reading with a Coil membership.

`+"\n\n", 1) } } } else { // We've viewing the post on the collection landing if spl > -1 { baseURL := c.CanonicalURL() if isOwner { baseURL = "/" + c.Alias + "/" } p.Content = p.Content[:spl+len(shortCodePaid)] p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:spl]), baseURL, cfg)) } } } } func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool, isPostPage bool) { baseURL := c.CanonicalURL() // TODO: redundant if !isSingleUser { baseURL = "/" + c.Alias + "/" } p.handlePremiumContent(c, isOwner, isPostPage, cfg) p.Content = strings.Replace(p.Content, "<!--paid-->", "", 1) - p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String))) + p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String), cfg)) p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), baseURL, cfg)) if exc := strings.Index(string(p.Content), ""); exc > -1 { p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:exc]), baseURL, cfg)) } } func (p *PublicPost) formatContent(cfg *config.Config, isOwner bool, isPostPage bool) { p.Post.formatContent(cfg, &p.Collection.Collection, isOwner, isPostPage) } func (p *Post) augmentContent(c *Collection) { if p.PinnedPosition.Valid { // Don't augment posts that are pinned return } if strings.Index(p.Content, "") > -1 { // Don't augment posts with the special "nosig" shortcode return } // Add post signatures if c.Signature != "" { p.Content += "\n\n" + c.Signature } } func (p *PublicPost) augmentContent() { p.Post.augmentContent(&p.Collection.Collection) } func (p *PublicPost) augmentReadingDestination() { if p.IsPaid { p.HTMLContent += template.HTML("\n\n" + `

` + localStr("Read more...", p.Language.String) + ` ($)

`) } } func applyMarkdown(data []byte, baseURL string, cfg *config.Config) string { - return applyMarkdownSpecial(data, false, baseURL, cfg) + if cfg.App.MarkdownRenderer() == "goldmark" { + return applyCommonmarkSpecial(data, false, baseURL, cfg) + } + return applySaturdaySpecial(data, false, baseURL, cfg) } func disableYoutubeAutoplay(outHTML string) string { for _, match := range youtubeReg.FindAllString(outHTML, -1) { u, err := url.Parse(match) if err != nil { continue } u.RawQuery = html.UnescapeString(u.RawQuery) q := u.Query() // Set Youtube autoplay url parameter, if any, to 0 if len(q["autoplay"]) == 1 { q.Set("autoplay", "0") } u.RawQuery = q.Encode() cleanURL := u.String() outHTML = strings.Replace(outHTML, match, cleanURL, 1) } return outHTML } +type hashtagResolver struct { + Prefix string +} + +var _ hashtag.Resolver = hashtagResolver{} + +func (h hashtagResolver) ResolveHashtag(node *hashtag.Node) (destination []byte, err error) { + var buf bytes.Buffer + buf.WriteString(h.Prefix) + buf.Write(node.Tag) + return buf.Bytes(), nil +} + func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *config.Config) string { + if cfg.App.MarkdownRenderer() == "goldmark" { + return applyCommonmarkSpecial(data, skipNoFollow, baseURL, cfg) + } else { + return applySaturdaySpecial(data, skipNoFollow, baseURL, cfg) + } +} + +func applyCommonmarkSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *config.Config) string { + extensions := cfg.App.RendererExtensions() + if baseURL != "" { + tagPrefix := baseURL + "tag:" + if cfg.App.Chorus { + tagPrefix = "/read/t" + } + extensions = append(extensions, &hashtag.Extender{ + Resolver: hashtagResolver{Prefix: tagPrefix}, + }) + } + md := goldmark.New( + goldmark.WithExtensions(extensions...), + goldmark.WithParserOptions( + parser.WithAutoHeadingID(), + ), + ) + var buf bytes.Buffer + if err := md.Convert(data, &buf); err != nil { + log.Info("error rendering CommonMark: %v", err) + } + htm := buf.Bytes() + if baseURL != "" { + handlePrefix := cfg.App.Host + "/@/" + htm = []byte(mentionReg.ReplaceAll(htm, []byte("@$1$2"))) + } + // Strip out bad HTML + policy := getSanitizationPolicy() + // Enable GFM checkboxes for CommonMark + // Technically we could skip this if the + policy.AllowAttrs("type", "disabled", "checked").OnElements("input") + policy.RequireNoFollowOnLinks(!skipNoFollow) + outHTML := string(policy.SanitizeBytes(htm)) + // Strip newlines on certain block elements that render with them + outHTML = blockReg.ReplaceAllString(outHTML, "<$1>") + outHTML = endBlockReg.ReplaceAllString(outHTML, "") + outHTML = disableYoutubeAutoplay(outHTML) + return outHTML +} + +func applySaturdaySpecial(data []byte, skipNoFollow bool, baseURL string, cfg *config.Config) string { mdExtensions := 0 | blackfriday.EXTENSION_TABLES | blackfriday.EXTENSION_FENCED_CODE | blackfriday.EXTENSION_AUTOLINK | blackfriday.EXTENSION_STRIKETHROUGH | blackfriday.EXTENSION_SPACE_HEADERS | blackfriday.EXTENSION_AUTO_HEADER_IDS htmlFlags := 0 | blackfriday.HTML_USE_SMARTYPANTS | blackfriday.HTML_SMARTYPANTS_DASHES if baseURL != "" { htmlFlags |= blackfriday.HTML_HASHTAGS } // Generate Markdown md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) if baseURL != "" { // Replace special text generated by Markdown parser tagPrefix := baseURL + "tag:" if cfg.App.Chorus { tagPrefix = "/read/t/" } md = []byte(hashtagReg.ReplaceAll(md, []byte("#$1"))) handlePrefix := cfg.App.Host + "/@/" md = []byte(mentionReg.ReplaceAll(md, []byte("@$1$2"))) } // Strip out bad HTML policy := getSanitizationPolicy() policy.RequireNoFollowOnLinks(!skipNoFollow) outHTML := string(policy.SanitizeBytes(md)) // Strip newlines on certain block elements that render with them outHTML = blockReg.ReplaceAllString(outHTML, "<$1>") outHTML = endBlockReg.ReplaceAllString(outHTML, "") outHTML = disableYoutubeAutoplay(outHTML) return outHTML } -func applyBasicMarkdown(data []byte) string { +func applyBasicMarkdown(data []byte, cfg *config.Config) string { + if cfg.App.MarkdownRenderer() == "goldmark" { + return applyBasicCommonmark(data, cfg) + } else { + return applyBasicSaturday(data) + } +} + +func applyBasicCommonmark(data []byte, cfg *config.Config) string { + md := goldmark.New( + goldmark.WithExtensions( + cfg.App.RendererExtensions()..., + ), + ) + var inbuf bytes.Buffer + inbuf.WriteString("# ") + inbuf.Write(data) + var outbuf bytes.Buffer + if err := md.Convert(inbuf.Bytes(), &outbuf); err != nil { + log.Info("error rendering basic CommonMark: %v", err) + } + htm := outbuf.Bytes() + htm = bytes.TrimSpace(htm) + + htm = htm[len("

") : len(htm)-len("

")] + // Strip out bad HTML + policy := bluemonday.UGCPolicy() + policy.AllowAttrs("class", "id").Globally() + outHTML := string(policy.SanitizeBytes(htm)) + outHTML = markeddownReg.ReplaceAllString(outHTML, "$1") + outHTML = strings.TrimRightFunc(outHTML, unicode.IsSpace) + return outHTML +} + +func applyBasicSaturday(data []byte) string { if len(bytes.TrimSpace(data)) == 0 { return "" } mdExtensions := 0 | blackfriday.EXTENSION_STRIKETHROUGH | blackfriday.EXTENSION_SPACE_HEADERS | blackfriday.EXTENSION_HEADER_IDS htmlFlags := 0 | blackfriday.HTML_SKIP_HTML | blackfriday.HTML_USE_SMARTYPANTS | blackfriday.HTML_SMARTYPANTS_DASHES // Generate Markdown // This passes the supplied title into blackfriday.Markdown() as an H1 header, so we only render HTML that // belongs in an H1. md := blackfriday.Markdown(append([]byte("# "), data...), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) // Remove H1 markup md = bytes.TrimSpace(md) // blackfriday.Markdown adds a newline at the end of the

if len(md) == 0 { return "" } md = md[len("

") : len(md)-len("

")] // Strip out bad HTML policy := bluemonday.UGCPolicy() policy.AllowAttrs("class", "id").Globally() outHTML := string(policy.SanitizeBytes(md)) outHTML = markeddownReg.ReplaceAllString(outHTML, "$1") outHTML = strings.TrimRightFunc(outHTML, unicode.IsSpace) return outHTML } func postTitle(content, friendlyId string) string { const maxTitleLen = 80 content = stripHTMLWithoutEscaping(content) content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace) eol := strings.IndexRune(content, '\n') blankLine := strings.Index(content, "\n\n") if blankLine != -1 && blankLine <= eol && blankLine <= assumedTitleLen { return strings.TrimSpace(content[:blankLine]) } else if utf8.RuneCountInString(content) <= maxTitleLen { return content } return friendlyId } // TODO: fix duplicated code from postTitle. postTitle is a widely used func we // don't have time to investigate right now. func friendlyPostTitle(content, friendlyId string) string { const maxTitleLen = 80 content = stripHTMLWithoutEscaping(content) content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace) eol := strings.IndexRune(content, '\n') blankLine := strings.Index(content, "\n\n") if blankLine != -1 && blankLine <= eol && blankLine <= assumedTitleLen { return strings.TrimSpace(content[:blankLine]) } else if eol == -1 && utf8.RuneCountInString(content) <= maxTitleLen { return content } title, truncd := parse.TruncToWord(parse.PostLede(content, true), maxTitleLen) if truncd { title += "..." } return title } // Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML // entities added in by sanitizing the content. func stripHTMLWithoutEscaping(content string) string { return html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content)) } func getSanitizationPolicy() *bluemonday.Policy { policy := bluemonday.UGCPolicy() policy.AllowAttrs("src", "style").OnElements("iframe", "video", "audio") policy.AllowAttrs("src", "type").OnElements("source") policy.AllowAttrs("frameborder", "width", "height").Matching(bluemonday.Integer).OnElements("iframe") policy.AllowAttrs("allowfullscreen").OnElements("iframe") policy.AllowAttrs("controls", "loop", "muted", "autoplay").OnElements("video") policy.AllowAttrs("controls", "loop", "muted", "autoplay", "preload").OnElements("audio") policy.AllowAttrs("target").OnElements("a") policy.AllowAttrs("title").OnElements("abbr") policy.AllowAttrs("style", "class", "id").Globally() policy.AllowAttrs("alt").OnElements("img") policy.AllowElements("header", "footer") policy.AllowURLSchemes("http", "https", "mailto", "xmpp") return policy } func sanitizePost(content string) string { return strings.Replace(content, "<", "<", -1) } // postDescription generates a description based on the given post content, // title, and post ID. This doesn't consider a V2 post field, `title` when // choosing what to generate. In case a post has a title, this function will // fail, and logic should instead be implemented to skip this when there's no // title, like so: // // var desc string // if title == "" { // desc = postDescription(content, title, friendlyId) // } else { // desc = shortPostDescription(content) // } func postDescription(content, title, friendlyId string) string { maxLen := 140 if content == "" { content = "WriteFreely is a painless, simple, federated blogging platform." } else { fmtStr := "%s" truncation := 0 if utf8.RuneCountInString(content) > maxLen { // Post is longer than the max description, so let's show a better description fmtStr = "%s..." truncation = 3 } if title == friendlyId { // No specific title was found; simply truncate the post, starting at the beginning content = fmt.Sprintf(fmtStr, strings.Replace(stringmanip.Substring(content, 0, maxLen-truncation), "\n", " ", -1)) } else { // There was a title, so return a real description blankLine := strings.Index(content, "\n\n") if blankLine < 0 { blankLine = 0 } truncd := stringmanip.Substring(content, blankLine, blankLine+maxLen-truncation) contentNoNL := strings.Replace(truncd, "\n", " ", -1) content = strings.TrimSpace(fmt.Sprintf(fmtStr, contentNoNL)) } } return content } func shortPostDescription(content string) string { maxLen := 140 fmtStr := "%s" truncation := 0 if utf8.RuneCountInString(content) > maxLen { // Post is longer than the max description, so let's show a better description fmtStr = "%s..." truncation = 3 } return strings.TrimSpace(fmt.Sprintf(fmtStr, strings.Replace(stringmanip.Substring(content, 0, maxLen-truncation), "\n", " ", -1))) } func handleRenderMarkdown(app *App, w http.ResponseWriter, r *http.Request) error { if !IsJSON(r) { return impart.HTTPError{Status: http.StatusUnsupportedMediaType, Message: "Markdown API only supports JSON requests"} } in := struct { CollectionURL string `json:"collection_url"` RawBody string `json:"raw_body"` }{} decoder := json.NewDecoder(r.Body) err := decoder.Decode(&in) if err != nil { log.Error("Couldn't parse markdown JSON request: %v", err) return ErrBadJSON } out := struct { Body string `json:"body"` }{ Body: applyMarkdown([]byte(in.RawBody), in.CollectionURL, app.cfg), } return impart.WriteSuccess(w, out, http.StatusOK) } diff --git a/postrender_test.go b/postrender_test.go index ec6bbdd..3ac2a77 100644 --- a/postrender_test.go +++ b/postrender_test.go @@ -1,43 +1,54 @@ /* * Copyright © 2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely -import "testing" +import ( + "testing" + + "github.com/writefreely/writefreely/config" +) func TestApplyBasicMarkdown(t *testing.T) { tests := []struct { name string in string result string }{ {"empty", "", ""}, {"empty spaces", " ", ""}, {"empty tabs", "\t", ""}, {"empty newline", "\n", ""}, {"nums", "123", "123"}, {"dot", ".", "."}, {"dash", "-", "-"}, {"plain", "Hello, World!", "Hello, World!"}, {"multibyte", "こんにちは", `こんにちは`}, {"bold", "**안녕하세요**", `안녕하세요`}, {"link", "[WriteFreely](https://writefreely.org)", `WriteFreely`}, {"date", "12. April", `12. April`}, {"table", "| Hi | There |", `| Hi | There |`}, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - res := applyBasicMarkdown([]byte(test.in)) - if res != test.result { - t.Errorf("%s: wanted %s, got %s", test.name, test.result, res) - } - }) + for _, renderer := range []string{"saturday", "goldmark"} { + cfg := &config.Config{ + App: config.AppCfg{ + Renderer: renderer, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + res := applyBasicMarkdown([]byte(test.in), cfg) + if res != test.result { + t.Errorf("%s: wanted %s, got %s", test.name, test.result, res) + } + }) + } } } diff --git a/templates.go b/templates.go index 9f4b59b..d4be9ec 100644 --- a/templates.go +++ b/templates.go @@ -1,266 +1,268 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "errors" "fmt" "html/template" "io" "os" "net/http" "path/filepath" "strings" "github.com/dustin/go-humanize" "github.com/writeas/web-core/l10n" "github.com/writeas/web-core/log" + "github.com/writefreely/writefreely/config" ) var ( templates = map[string]*template.Template{} pages = map[string]*template.Template{} userPages = map[string]*template.Template{} funcMap = template.FuncMap{ "largeNumFmt": largeNumFmt, "pluralize": pluralize, "isRTL": isRTL, "isLTR": isLTR, "localstr": localStr, "localhtml": localHTML, "tolower": strings.ToLower, "title": strings.Title, "hasPrefix": strings.HasPrefix, "hasSuffix": strings.HasSuffix, "dict": dict, } ) const ( templatesDir = "templates" pagesDir = "pages" ) func showUserPage(w http.ResponseWriter, name string, obj interface{}) { if obj == nil { log.Error("showUserPage: data is nil!") return } if err := userPages[filepath.Join("user", name+".tmpl")].ExecuteTemplate(w, name, obj); err != nil { log.Error("Error parsing %s: %v", name, err) } } func initTemplate(parentDir, name string) { if debugging { log.Info(" " + filepath.Join(parentDir, templatesDir, name+".tmpl")) } files := []string{ filepath.Join(parentDir, templatesDir, name+".tmpl"), filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"), } if name == "collection" || name == "collection-tags" || name == "chorus-collection" || name == "read" { // These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl" files = append(files, filepath.Join(parentDir, templatesDir, "include", "posts.tmpl")) } if name == "chorus-collection" || name == "chorus-collection-post" { files = append(files, filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl")) } if name == "collection" || name == "collection-tags" || name == "collection-post" || name == "post" || name == "chorus-collection" || name == "chorus-collection-post" { files = append(files, filepath.Join(parentDir, templatesDir, "include", "post-render.tmpl")) } templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...)) } func initPage(parentDir, path, key string) { if debugging { log.Info(" [%s] %s", key, path) } files := []string{ path, filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"), } if key == "login.tmpl" || key == "landing.tmpl" || key == "signup.tmpl" { files = append(files, filepath.Join(parentDir, templatesDir, "include", "oauth.tmpl")) } pages[key] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...)) } func initUserPage(parentDir, path, key string) { if debugging { log.Info(" [%s] %s", key, path) } userPages[key] = template.Must(template.New(key).Funcs(funcMap).ParseFiles( path, filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "nav.tmpl"), )) } // InitTemplates loads all template files from the configured parent dir. func InitTemplates(cfg *config.Config) error { var templatesPath string = filepath.Join(cfg.Server.TemplatesParentDir, templatesDir) if _, err := os.Stat(templatesPath); os.IsNotExist(err) { var path string = templatesPath s, err := filepath.Abs(templatesPath) if nil == err { path = s } return fmt.Errorf("the directory for the 'templates/' templates does not exist — should have been at %q", path) } log.Info("Loading templates...") tmplFiles, err := os.ReadDir(templatesPath) if err != nil { return err } for _, f := range tmplFiles { if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") { parts := strings.Split(f.Name(), ".") key := parts[0] initTemplate(cfg.Server.TemplatesParentDir, key) } } var pagesPath string = filepath.Join(cfg.Server.PagesParentDir, pagesDir) if _, err := os.Stat(pagesPath); os.IsNotExist(err) { var path string = pagesPath s, err := filepath.Abs(pagesPath) if nil == err { path = s } return fmt.Errorf("the directory for the 'pages/' templates does not exist — should have been at %q", path) } log.Info("Loading pages...") // Initialize all static pages that use the base template err = filepath.Walk(pagesPath, func(path string, i os.FileInfo, err error) error { if err != nil { return fmt.Errorf("problem loading template from %q: %w", path, err) } - if !i.IsDir() && !strings.HasPrefix(i.Name(), ".") { + + if i != nil && !i.IsDir() && !strings.HasPrefix(i.Name(), ".") { key := i.Name() initPage(cfg.Server.PagesParentDir, path, key) } return nil }) if err != nil { return err } log.Info("Loading user pages...") // Initialize all user pages that use base templates err = filepath.Walk(filepath.Join(cfg.Server.TemplatesParentDir, templatesDir, "user"), func(path string, f os.FileInfo, err error) error { if err != nil { return err } if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") { corePath := path if cfg.Server.TemplatesParentDir != "" { corePath = corePath[len(cfg.Server.TemplatesParentDir)+1:] } parts := strings.Split(corePath, string(filepath.Separator)) key := f.Name() if len(parts) > 2 { key = filepath.Join(parts[1], f.Name()) } initUserPage(cfg.Server.TemplatesParentDir, path, key) } return nil }) if err != nil { return err } return nil } // renderPage retrieves the given template and renders it to the given io.Writer. // If something goes wrong, the error is logged and returned. func renderPage(w io.Writer, tmpl string, data interface{}) error { err := pages[tmpl].ExecuteTemplate(w, "base", data) if err != nil { log.Error("%v", err) } return err } func largeNumFmt(n int64) string { return humanize.Comma(n) } func pluralize(singular, plural string, n int64) string { if n == 1 { return singular } return plural } func isRTL(d string) bool { return d == "rtl" } func isLTR(d string) bool { return d == "ltr" || d == "auto" } func localStr(term, lang string) string { s := l10n.Strings(lang)[term] if s == "" { s = l10n.Strings("")[term] } return s } func localHTML(term, lang string) template.HTML { s := l10n.Strings(lang)[term] if s == "" { s = l10n.Strings("")[term] } s = strings.Replace(s, "write.as", "writefreely", 1) return template.HTML(s) } // from: https://stackoverflow.com/a/18276968/1549194 func dict(values ...interface{}) (map[string]interface{}, error) { if len(values)%2 != 0 { return nil, errors.New("dict: invalid number of parameters") } dict := make(map[string]interface{}, len(values)/2) for i := 0; i < len(values); i += 2 { key, ok := values[i].(string) if !ok { return nil, errors.New("dict: keys must be strings") } dict[key] = values[i+1] } return dict, nil }